package com.ideaaedi.notebook;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * 构建图片
 *
 * @author JustryDeng
 * @since 2021/7/28 0:53:01
 */
public class Notebook {
    
    /**
     * 构建图片
     * <p>
     *     若不构建，预览.md文件时，可能加载图片失败
     * </p>
     */
    public static void buildImages() {
        String projectRootDir = getProjectRootDir();
        // 只有jar包才会进行build逻辑
        if (!projectRootDir.endsWith(".jar")) {
            System.err.println("Only jar file need buildImages.");
            return;
        }
        File file = new File(projectRootDir);
        String parent = file.getParent();
        parent = parent == null ? "/" : parent;
        System.out.println("Build images to " + new File(parent, "repository").getAbsolutePath() + ".");
        unJarWar(projectRootDir, parent, Collections.singletonList("repository/"));
        System.out.println("Build images completed.");
    }
    
    /**
     * 获取clazz类的全类名对应包的根路径
     *
     * <ul>
     *     <li>如果编译后未打包时直接调用这个方法(即:直接在开发工具里调用此方法)，那么结果形如:   D:/资料整理/demo模板/class-winter/class-winter-core/target/classes/</li>
     *     <li>如果是jar包运行时，(clazz位于jar包中，)调用这个方法，(假设jar包位置是D:/资料整理/demo模板/abc.jar)那么结果形如：   D:/资料整理/demo模板/abc.jar</li>
     * </ul>
     *
     * @return  项目根目录
     */
    private static String getProjectRootDir() {
        URL url = Notebook.class.getProtectionDomain().getCodeSource().getLocation();
        String filePath;
        try {
            filePath = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        File file = new File(filePath);
        filePath = file.getAbsolutePath().replace("\\", "/");
        // 如果file是文件夹，那么保证filePath是以/结尾的
        if (file.isDirectory() && !filePath.endsWith("/")) {
            filePath = filePath + "/";
        }
        return filePath;
    }
    
    /**
     * 解压jar(or war)至指定的目录
     *
     * @param jarWarPath
     *            待解压的jar(or war)文件
     * @param targetDir
     *            解压后文件放置的文件夹
     * @param entryNamePrefixes
     *            只有当entryName为指定的前缀时，才对该entry进行解压(若为null或者长度为0， 则解压所有文件)   如: ["BOOT-INF/classes/", "BOOT-INF/classes/com/example/ssm/author/JustryDeng.class"]
     *            <br/>
     *            注:当entry对应jar或者war中的目录时，那么其值形如 BOOT-INF/classes/
     *            <br/>
     *            注:当entry对应jar或者war中的文件时，那么其值形如 BOOT-INF/classes/com/example/ssm/author/JustryDeng.class
     * @return  解压出来的文件(包含目录)的完整路径
     */
    private static <T extends Collection<String>> List<String> unJarWar(String jarWarPath, String targetDir,
                                                                       T entryNamePrefixes) {
        List<String> list = new ArrayList<>();
        File target = new File(targetDir);
        guarantyDirExist(target);
        
        ZipFile zipFile = null;
        try {
            zipFile = new ZipFile(new File(jarWarPath));
            ZipEntry entry;
            File targetFile;
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                entry = entries.nextElement();
                String entryName = entry.getName();
                // 若entryNamePrefixes不为空，则不解压前缀不匹配的文件或文件夹
                if (entryNamePrefixes != null && entryNamePrefixes.size() > 0
                        && entryNamePrefixes.stream().noneMatch(entryName::startsWith)) {
                    continue;
                }
                if (entry.isDirectory()) {
                    targetFile = new File(target, entryName);
                    guarantyDirExist(targetFile);
                } else {
                    // 有时遍历时，文件先于文件夹出来，所以也得保证目录存在
                    int lastSeparatorIndex = entryName.lastIndexOf("/");
                    if (lastSeparatorIndex > 0) {
                        guarantyDirExist(new File(target, entryName.substring(0, lastSeparatorIndex)));
                    }
                    // 解压文件
                    targetFile = new File(target, entryName);
                    byte[] bytes = toBytes(zipFile.getInputStream(entry));
                    toFile(bytes, targetFile);
                    list.add(targetFile.getAbsolutePath());
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            close(zipFile);
        }
        return list;
    }
    
    /**
     * 保证目录存在
     *
     * @param dir
     *            目录
     */
    private static void guarantyDirExist(File dir) {
        if (!dir.exists()) {
            //noinspection ResultOfMethodCallIgnored
            dir.mkdirs();
        }
    }
    
    /**
     * 将srcFileBytes写出为destFile文件
     * <p>
     *     注: 若源文件存在，则会覆盖原有的内容。
     * </p>
     *  @param srcFileBytes
     *            字节
     * @param destFile
     *            文件
     */
    private static void toFile(byte[] srcFileBytes, File destFile) {
        OutputStream os = null;
        try {
            if (destFile.isDirectory()) {
                throw new RuntimeException("destFile [" + destFile.getAbsolutePath() + "] must be file rather than dir.");
            }
            
            if (!destFile.exists()) {
                File parentFile = destFile.getParentFile();
                if (!parentFile.exists()) {
                    //noinspection ResultOfMethodCallIgnored
                    parentFile.mkdirs();
                }
                //noinspection ResultOfMethodCallIgnored
                destFile.createNewFile();
            } else if (!destFile.exists()) {
                throw new IllegalArgumentException("destFile [" + destFile.getAbsolutePath() + "] non exist.");
            }
            os = new FileOutputStream(destFile);
            os.write(srcFileBytes, 0, srcFileBytes.length);
            os.flush();
        } catch (IOException e) {
            throw new RuntimeException(" toFile [" + destFile.getAbsolutePath() + "] occur exception.", e);
        } finally {
            close(os);
        }
    }
    
    /**
     * 关闭流
     *
     * @param ioArr
     *            待关闭的io
     */
    private static void close(Closeable... ioArr) {
        if (ioArr == null) {
            return;
        }
        for (Closeable io : ioArr) {
            if (io == null) {
                continue;
            }
            try {
                io.close();
            } catch (IOException e) {
                // ignore
            }
        }
    }
    
    /**
     * 将inputStream转换为byte[]
     * <p>
     *     注：此方法会释放inputStream
     * </p>
     *
     * @param inputStream
     *            输入流
     * @return  字节
     */
    private static byte[] toBytes(InputStream inputStream) throws IOException {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try {
            byte[] buffer = new byte[4096];
            int n;
            while (-1 != (n = inputStream.read(buffer))) {
                output.write(buffer, 0, n);
            }
            return output.toByteArray();
        } finally {
            close(output, inputStream);
        }
    }
}
